-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add improve-conditionals check in the CodeStyle extension #10600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
"R6106": ( | ||
"Rewrite conditional expression to '%s'", | ||
"improve-conditionals", | ||
"Rewrite negated if expressions to improve readability.", | ||
{ | ||
# "default_enabled": False, | ||
}, | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could probably be improved. Open to suggestions.
We could also change the checker name if someone has a good idea.
Besides that, should the checker be disabled by default even in the optional CodeStyle
extension?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either that or it should be a pylint plugin. I regret including magic-number-comparison because then Ruff implemented it and Ruff does not take the default disable into account and it makes pylint look really opinionated and unreasonable.
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (89.69%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #10600 +/- ##
==========================================
- Coverage 95.95% 95.92% -0.04%
==========================================
Files 176 176
Lines 19476 19525 +49
==========================================
+ Hits 18689 18730 +41
- Misses 787 795 +8
🚀 New features to boost your workflow:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing the doc example with animal made me reconsider if the suggestion is always more readable. I guess when there's a double negation not x or not y
, then yes, not (x and y)
is better for not x or y >= 0
then not (x and y < 0)
is slightly better, otherwise I don't know. The primer also point to an especially opinionated check that could be an external plugin. Let us hear from others.
"R6106": ( | ||
"Rewrite conditional expression to '%s'", | ||
"improve-conditionals", | ||
"Rewrite negated if expressions to improve readability.", | ||
{ | ||
# "default_enabled": False, | ||
}, | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either that or it should be a pylint plugin. I regret including magic-number-comparison because then Ruff implemented it and Ruff does not take the default disable into account and it makes pylint look really opinionated and unreasonable.
def func(expr, node_cls): | ||
# +1:[improve-conditionals] | ||
if not isinstance(expr, node_cls) or expr.attrname != "__init__": | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def func(expr, node_cls): | |
# +1:[improve-conditionals] | |
if not isinstance(expr, node_cls) or expr.attrname != "__init__": | |
... | |
def is_platypus(animal): | |
# The platypus is both the only mammal with a beak and without nipples. | |
# +1:[improve-conditionals] | |
return animal.is_mammal() and (not animal.has_nipples() or animal.has_beak) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your example wouldn't actually raise the message. Yes, it could be inverted but so far I've chosen not to emit it if we'd need to add not
, only if it could be removed / moved before the BoolOp. I.e. it would only be emitted for
# bad
x and (not y or not z)
# good
x and not (y and z)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok the checker is a lot less opinionated than I thought then : good.
I had to do some extensive phenology for this one, turn out uniquely identifying an animal by lack of a characteristic is not so easy.
def func(expr, node_cls): | |
# +1:[improve-conditionals] | |
if not isinstance(expr, node_cls) or expr.attrname != "__init__": | |
... | |
def is_penguin(animal): | |
# Penguins are the only flightless, kneeless sea birds | |
# +1:[improve-conditionals] | |
return animal.is_seabird() and (not animal.can_fly() or not animal.has_visible_knee()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok the checker is a lot less opinionated than I thought then : good.
I went through all of the warning for Home Assistant. They looked quite good. There was just one common pattern I don't think the checker should emit a warning for.
# is x between 0 and 100
if x > 0 or x < 100:
...
Addressed that one in b08beb4
pylint/extensions/private_import.py
Outdated
bool(name) | ||
and name[0] == "_" | ||
and (len(name) <= 4 or name[1] != "_" or name[-2:] != "__") | ||
and not (len(name) > 4 and name[:2] == "__" and name[-2:] == "__") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition was done with great care about performance here, I'm not sure changing the order without benchmark is advisable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are they any different though? Let's consider some cases
Returning True
To return True
with the "old" code, any one of the conditions had to evaluate to true and or
short-circuits the conditional. With the "new" one it's just the inverse. Any one condition has to eval to false and and
short-circuits as well. That's fine as all comparisons are inverted itself.
Returning False
With or
all conditions had to be checked to make sure none actually returned true. Same with and
. It will only return false if all conditions eval to true.
--
Am I missing something here? Tbh I had to think hard about it and even thought they aren't identical for a few minutes. In the end however, they should evaluate almost exactly the same.
--
I reverted the same change to the second check with name[:2]
(instead of name[1]
) in 7e4bb03.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at it too fast and thought that the prior check for first char is _ was removed, but it's not. We already checked the first character so name[:2] == "__" should stay name[1] == "_". And in this case it become equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+0.5 for deactivated by default in the code style checker. I invested so much time in the biology involved in the doc example that I now have sunk cost fallacy about it.
Co-authored-by: Pierre Sassoulas <[email protected]>
840dfbf
to
7e4bb03
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this check is ready now. Yes, it's a bit opinionated but that could even be a positive thing. In the end it'll be disabled by default in an optional checker.
At last I just want to highlight one of the cases I found in Home Assistant. There aren't a lot of them but sometimes it can be just art 🧑🎨
# current
if (
not isinstance(data, dict)
or "eventType" not in data
or data["eventType"] != "changeReport"
or "eventVersion" not in data
or data["eventVersion"] != "1"
or "context" not in data
or not isinstance(data["context"], dict)
or "deviceType" not in data["context"]
or "deviceMac" not in data["context"]
):
return
# with 'and' instead of 'or' it gets obvious that this might be a great place for match
if not (
isinstance(data, dict)
and "eventType" in data
and data["eventType"] == "changeReport"
and "eventVersion" in data
and data["eventVersion"] == "1"
and "context" in data
and isinstance(data["context"], dict)
and "deviceType" in data["context"]
and "deviceMac" in data["context"]
):
return
match data:
case {
"eventType": "changeReport",
"eventVersion": "1",
"context": {"deviceType": _, "deviceMac": _},
}:
pass
case _:
return
def func(expr, node_cls): | ||
# +1:[improve-conditionals] | ||
if not isinstance(expr, node_cls) or expr.attrname != "__init__": | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok the checker is a lot less opinionated than I thought then : good.
I went through all of the warning for Home Assistant. They looked quite good. There was just one common pattern I don't think the checker should emit a warning for.
# is x between 0 and 100
if x > 0 or x < 100:
...
Addressed that one in b08beb4
pylint/extensions/private_import.py
Outdated
bool(name) | ||
and name[0] == "_" | ||
and (len(name) <= 4 or name[1] != "_" or name[-2:] != "__") | ||
and not (len(name) > 4 and name[:2] == "__" and name[-2:] == "__") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are they any different though? Let's consider some cases
Returning True
To return True
with the "old" code, any one of the conditions had to evaluate to true and or
short-circuits the conditional. With the "new" one it's just the inverse. Any one condition has to eval to false and and
short-circuits as well. That's fine as all comparisons are inverted itself.
Returning False
With or
all conditions had to be checked to make sure none actually returned true. Same with and
. It will only return false if all conditions eval to true.
--
Am I missing something here? Tbh I had to think hard about it and even thought they aren't identical for a few minutes. In the end however, they should evaluate almost exactly the same.
--
I reverted the same change to the second check with name[:2]
(instead of name[1]
) in 7e4bb03.
🤖 Effect of this PR on checked open source code: 🤖 Effect on home-assistant:
This comment was truncated because GitHub allows only 65536 characters in a comment. This comment was generated for commit 87e5786 |
@@ -0,0 +1,3 @@ | |||
Add :ref:`improve-conditionals` check to the Code Style extension. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add :ref:`improve-conditionals` check to the Code Style extension. | |
Add :ref:`improve-conditionals` check to the Code Style extension to suggest `not(x and y)` instead of `not x or not y` in order to facilitate the detection of match case refactors. The code style extension must be enabled and `improve-conditionals` itself needs to be explicitely enabled. |
), | ||
"R6106": ( | ||
"Rewrite conditional expression to '%s'", | ||
"improve-conditionals", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is a little vague. What about consider-factorizing-not, 'or-with-double-negation'... ? I'm on mobile but asking a llm about it might give the least surprising name for it.
The exemple you give does make sense. What if we were raising only strictly above 2 negated conditions ? It's not the same purpose than the initial idea but it would make thé check a lot less opinionated |
Or say we add an option for the threshold of negated conditions, and activate the message by default with a value of 4 |
Followup to #10581
Suggest rewriting conditional expressions
I've added the check in the
CodeStyle
extension. It might even make sense to disable it by default.